/*
This file is part of maven-bugzilla-plugin.

    maven-bugzilla-plugin is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    maven-bugzilla-plugin is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with maven-bugzilla-plugin.  If not, see <http://www.gnu.org/licenses/>.
 */
package nl.semlab.maven.plugin.bugzilla;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.xmlbeans.XmlException;

import nl.semlab.maven.plugin.bugzilla.exceptions.BugzillaException;
import nl.semlab.maven.plugin.bugzilla.exceptions.FindBugsException;

import noNamespace.BugCollectionDocument;
import noNamespace.BugCollectionDocument.BugCollection;
import noNamespace.BugCollectionDocument.BugCollection.BugInstance;
import noNamespace.SourceLineDocument.SourceLine;

/**
 * Show a list of bugs found by Findbugs, which can be added to Bugzilla.
 * <p />
 * Project maven-bugzilla-plugin<br />
 * BugzillaFindbugsMojo.java created 09 Apr 2009
 * <p />
 * Copyright &copy 2009 SemLab
 * 
 * @author <a href="mailto:borsje@semlab.nl">J.A. Borsje</a>
 * @version $Revision: 1.1 $, $Date: 2009-08-03 10:04:21 $
 * @goal report-findbugs
 * @phase deploy
 */
public class BugzillaFindbugsMojo extends AbstractBugzillaMojo
{
   private static final String BUG_DESCRIPTION_URL = "http://findbugs.sourceforge.net/bugDescriptions.html#";
   private static final List<String> HANDLED_BUG_STATES = new ArrayList<String>();
   static
   {
      BugzillaFindbugsMojo.HANDLED_BUG_STATES.add("RESOLVED");
      BugzillaFindbugsMojo.HANDLED_BUG_STATES.add("VERIFIED");
      BugzillaFindbugsMojo.HANDLED_BUG_STATES.add("CLOSED");
   }

   /**
    * @parameter expression="${project.build.directory}"
    */
   protected String target;

   /**
    * @parameter default-value="findbugsXml.xml"
    */
   protected String findbugsXMLFile;

   /**
    * @parameter default-value="Findbugs"
    */
   protected String component;

   /**
    * A mapping between the priority of a bug in FindBugs and the severity of a
    * bug in Bugzilla.
    */
   private final Map<Short, String> prioritySeverityMapping;

   /**
    * Create the Bugzilla Findbugs mojo.
    */
   public BugzillaFindbugsMojo()
   {
      super();
      prioritySeverityMapping = new HashMap<Short, String>();
      prioritySeverityMapping.put((short) 1, "major");
      prioritySeverityMapping.put((short) 2, "normal");
      prioritySeverityMapping.put((short) 3, "minor");
      prioritySeverityMapping.put((short) 4, "trivial");
      prioritySeverityMapping.put((short) 5, "trivial");
   }

   /**
    * @see org.apache.maven.plugin.Mojo#execute()
    */
   public void execute()
   {
      // Only execute for JAR or maven-plugin projects.
      String type = project.getArtifact().getType();
      if ("jar".equals(type) || "maven-plugin".equals(type))
      {
         try
         {
            // Initialize the mojo.
            super.init();

            // Read the FindBugs XML.
            File file = this.getFindbugsXMLOutput();

            // Get the bugs.
            BugCollection bugs = BugCollectionDocument.Factory.parse(new FileReader(file)).getBugCollection();
            this.getLog().info(bugs.sizeOfBugInstanceArray() + " bugs found by FindBugs.");

            // Login to bugzilla.
            super.webservice.login();
            this.getLog().info("Login successful");

            // For each bug: check if this bug is already present in Bugzilla,
            // if not submit it.
            for (BugInstance bug : bugs.getBugInstanceArray())
            {
               try
               {
                  if (!super.webservice.hasDuplicate(this.getAlias(bug)))
                  {
                     Map<String, String> bugReport = this.createBug(bug);
                     Integer bugId = super.webservice.addBug(bugReport);
                     this.getLog().info("Bug added: " + bugId);
                  }
                  else
                  {
                     this.getLog()
                           .info("Bug with hash [" + bug.getInstanceHash() + "] is already present in bugzilla.");
                  }
               }
               catch (BugzillaException ex)
               {
                  this.getLog().warn(
                        "An error occurred while communicating with Bugzilla for bug: " + bug.getInstanceHash() + ".",
                        ex);
               }
            }
         }
         catch (FindBugsException ex)
         {
            this.getLog().warn("The Findbugs XML file could not be found.", ex);
         }
         catch (FileNotFoundException ex)
         {
            this.getLog().warn("The Findbugs XML file could not be found.", ex);
         }
         catch (XmlException ex)
         {
            this.getLog().warn("The Findbugs XML file could not be parsed.", ex);
         }
         catch (IOException ex)
         {
            this.getLog().warn("The Findbugs XML file could not be read.", ex);
         }
         catch (BugzillaException ex)
         {
            this.getLog().warn("Login to bugzilla failed.", ex);
         }
      }
   }

   /**
    * Get the Findbugs XML file.
    * 
    * @return The Findbugs XML file.
    * @throws FindBugsException
    *            Thrown if the Findbugs XML file could not be found.
    */
   private File getFindbugsXMLOutput() throws FindBugsException
   {
      File retval = new File(target, findbugsXMLFile);
      if (!retval.exists())
      {
         // TODO: run FindBugs if the XML file could not be found.
         throw new FindBugsException("The findbugs file [" + findbugsXMLFile + "] in [" + target + "] does not exist.");
      }
      return retval;
   }

   /**
    * Create the bug which can be send to Bugzilla using its Webservice API,
    * based on the FindBugs bug instance.
    * 
    * @param bug
    *           The FindBugs bug which needs to be converted to reported to
    *           Bugzilla.
    * @return A Bugzilla compatible bug.
    */
   private Map<String, String> createBug(BugInstance bug)
   {
      Map<String, String> retval = new HashMap<String, String>();

      // product (string) Required - The name of the product the bug is being
      // filed against.
      retval.put("product", product);
      super.getLog().debug("product: " + retval.get("product"));

      // component (string) Required - The name of a component in the product
      // above.
      retval.put("component", component);
      super.getLog().debug("component: " + retval.get("component"));

      // summary (string) Required - A brief description of the bug being filed.
      retval.put("summary", bug.getShortMessage());
      super.getLog().debug("summary: " + retval.get("summary"));

      // version (string) Required - A version of the product above; the version
      // the bug was found in.
      retval.put("version", version);
      super.getLog().debug("version: " + retval.get("version"));

      // description (string) Defaulted - The initial description for this bug.
      // Some Bugzilla installations require
      // this to not be blank.
      retval.put("description", this.getDescription(bug));

      // TODO: the alias is not used, because we do not do duplicate detection.
      // alias (string) - A brief alias for the bug that can be used instead of
      // a bug number when accessing this bug.
      // Must be unique in all of this Bugzilla.
      // retval.put("alias", this.getAlias(bug));

      // severity (string) Defaulted - How severe the bug is.
      retval.put("severity", this.getSeverity(bug.getPriority()));
      super.getLog().debug("severity: " + retval.get("severity"));

      return retval;
   }

   /**
    * Maps a FindBugs priority to a Bugzilla severity.
    * 
    * @param findBugsPriority
    *           The FindBugs priority.
    * @return The Bugzilla severity.
    */
   private String getSeverity(short findBugsPriority)
   {
      return prioritySeverityMapping.get(findBugsPriority);
   }

   /**
    * Get the description for the Bugzilla bug report.
    * 
    * @param bug
    *           The FindBugs bug instance.
    * @return The description used in the Bugzilla bug report.
    */
   private String getDescription(BugInstance bug)
   {
      StringBuilder retval = new StringBuilder();
      retval.append(bug.getLongMessage());
      retval.append("\n");
      retval.append("Source lines:\n");
      SourceLine[] sourceLines = bug.getSourceLineArray();
      for (SourceLine sourceLine : sourceLines)
      {
         retval.append("- ");
         retval.append(sourceLine.getMessage());
         retval.append("\n");
      }
      retval.append("\n");
      retval.append("Category: ");
      retval.append(bug.getCategory());
      retval.append("\n\n");
      retval.append("More information: ");
      retval.append(BugzillaFindbugsMojo.BUG_DESCRIPTION_URL);
      retval.append(bug.getType2());
      return retval.toString();
   }

   /**
    * Get the alias of a bug used in Bugzilla. This is based on the FindBugs
    * instance hash.
    * 
    * @param bug
    *           The bug for which to get the alias.
    * @return The alias used in Bugzilla.
    */
   private String getAlias(BugInstance bug)
   {
      StringBuilder base = new StringBuilder();
      base.append(bug.getInstanceHash());
      base.append(version);
      return "FB" + base.toString().hashCode();
   }
}